package jxerializeml.xml.reader;

import java.io.ByteArrayInputStream;
import java.io.IOException;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import jxerializeml.errors.input.CorruptedDocument;
import jxerializeml.errors.input.InvalidInputValue;
import jxerializeml.errors.input.XMLReaderError;
import jxerializeml.xml.NodeBean;
import jxerializeml.xml.transformers.InTransformer;
import jxerializeml.xml.transformers.InvalidTransformerException;

import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * <p>
 *   Reads XML document with serialized object, translates it using transformer
 *   and then calls listener, which should deserialize object according to given
 *   informations.
 * </p>
 * <p>
 *   Purpose of this class is to separate parsing of the XML document from actual
 *   process of deserialization.
 * </p>
 * 
 * @author Tomáš Zíma
 * @see    {@link XMLReaderListener}
 */
public class XMLReader {
	/**
	 * SAX parser reads document element-by-element and after each element, some
	 * sort of listener is triggered.
	 */
	private SAXParser saxParser = null;
	
	/** Translates NodeBeans to FieldBeans. */
	private InTransformer inTransformer = null;
	
	/** Generated XML document. */
	private String xmlDocument = null;
	
	/**
	 * Listener, which will be triggered for deserialization of each item (e.g.
	 * field).
	 */
	private XMLReaderListener listener = null;
	
	/**
	 * Prepares everything what is necessary for parsing process, but doesn't
	 * start it.
	 * 
	 * @param xmlDocument
	 *     String with whole content of the XML document (e.g. loaded from file).
	 *   
	 * @param inTransformer
	 *     Transformer, which will be used for translating of NodeBeans to the
	 *     FieldBeans.
	 *   
	 * @param listener
	 *     Listener, which will accept loaded informations and deserialize object
	 *     using that.
	 *   
	 * @throws XMLReaderError
	 *     This exception will be thrown if some critical error occurs while 
	 *     creating the parser. This might be caused by invalid configuration
	 *     of the parser or some related kind of error.
	 */
	public XMLReader(String xmlDocument, InTransformer inTransformer, XMLReaderListener listener) throws XMLReaderError {
		try {
    		this.saxParser     = SAXParserFactory.newInstance().newSAXParser();
    		this.inTransformer = inTransformer;
    		this.xmlDocument   = xmlDocument;
    		this.listener      = listener;
		} catch (ParserConfigurationException parserConfigurationException) {
			throw new XMLReaderError(
				"XMLReader couldn't be created due to invalid configuration of the"
				+ "XML parser, which is provided by the Java Core API.",
				
				parserConfigurationException
			);
		} catch (SAXException saxException) {
			throw new XMLReaderError(
				"Critical error has occured while parsing the document.",
				saxException
			);
		}
	}
	
	/**
	 * Starts parsing process. Listener will be triggered after each complete
	 * piece of information is loaded.
	 * 
	 * @throws XMLReaderError
	 *     This exception will be thrown if some I/O error has occurred or document
	 *     is corrupted.
	 */
	public void parse() throws XMLReaderError {
		try {
    		saxParser.parse(new ByteArrayInputStream(xmlDocument.getBytes()), new DefaultHandler() {
    			@Override
    			public void startElement(String uri, String localName, String tagName, Attributes attributes) throws SAXException {
    				// Creates new NodeBean and set it's tag name
    				NodeBean nodeBean = new NodeBean();
    				nodeBean.setTagName(tagName);
    				
    				// Copies all attributes into the NodeBean
    				for (int attributeId = 0; attributeId < attributes.getLength(); attributeId++) {
    					String attributeName    = attributes.getLocalName(attributeId);
    					String attributeValue   = attributes.getValue(attributeId);
    					
    					nodeBean.setAttribute(attributeName, attributeValue);
    				}
    
    				// Translates NodeBean in order to check what to do next
    				switch (inTransformer.translate(nodeBean)) {
    					// Create a new object
    					case OBJECT:
    						try {
    							listener.createObject(
    								inTransformer.transformRoot(nodeBean)
    							);
    						} catch (InvalidTransformerException err) {
    							this.fatalError(err);
    						}
    						
    						break;
    					
    					// Set up a field
    					case FIELD:
    						try {
        						listener.enterNode();
        						listener.setField(
        							inTransformer.transformField(nodeBean)
        						);
    						} catch (InvalidInputValue err) {
    							this.fatalError(err);
    						}
    						break;
    
    					// Set up an array item
    					case ARRAY_ITEM:
    						listener.enterNode();
    						listener.setArrayItem(
    							inTransformer.transformArrayItem(nodeBean)
    						);
    						break;
    					
    					// Corrupted document
    					case UNRECOGNIZED:
    						this.fatalError(
    							new CorruptedDocument(
    								"Some element couldn't be recognized and document is probably corrupted."
    							)
    						);
    				}
    			}
    			
    			@Override
    			public void endElement(String uri, String localName, String tagName) {
    				listener.leaveNode();
    			}
    		});
		} catch (IOException ioException) {
			throw new XMLReaderError(
				"Some I/O error has occured. This may mean that input stream is not readable.",
				ioException
			);
		} catch (SAXException saxException) {
			throw new XMLReaderError(
				"Some error has occured inside the parser or inside the XML handler. Call"
				+ "method getOriginalException() to get original error.",
				
				saxException
			);
        }
	}
}
